using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using PropertyExpression.Common;

namespace PropertyExpression.ControllableQuery
{
    public static class DynamicMaterializer
    {
        internal static readonly ModuleBuilder ModuleBuilder;
        internal static readonly AssemblyName AssemblyName;

        static DynamicMaterializer()
        {
            AssemblyName = new AssemblyName { Name = "A65c353d579264c5d86e55168b5a8d954" };
            ModuleBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(AssemblyName, AssemblyBuilderAccess.Run).
                DefineDynamicModule(AssemblyName.Name);
        }

        public static bool IsAnonymousType(Type type)
        {
            var customAttributes = type.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false);
            switch (customAttributes.Length)
            {
                case 0:
                    return false;
                case 1:
                    return type.Name.Contains("AnonymousType");
                default:
                    throw new ApplicationException();
            }
        }

        public static Func<T, object> BuildAnonymousTypeParamInference<T>()
        {
            var type = typeof (T);

            var dynamicMethod = new DynamicMethod(name: Guid.NewGuid().ToString("N"), returnType: typeof (object),
                parameterTypes: new[] {typeof (T)}, restrictedSkipVisibility: true);

            var ilGenerator = dynamicMethod.GetILGenerator();
            foreach (var propertyInfo in type.GetProperties())
            {
                ilGenerator.Emit(OpCodes.Ldarg_0);
                ilGenerator.EmitCall(OpCodes.Callvirt, propertyInfo.GetGetMethod(), null);
                ilGenerator.EmitCall(OpCodes.Call,
                    Util.ParamInferenceMethod.MakeGenericMethod(
                        propertyInfo.PropertyType, Util.ParamInferenceType(propertyInfo.PropertyType)), null);
            }
            ilGenerator.Emit(OpCodes.Newobj,
                (type.IsGenericType ? Util.ParamInferenceTypeForAnonymousType(type) : type)
                    .GetConstructors().Single());
            ilGenerator.Emit(OpCodes.Ret);

            dynamicMethod.DefineParameter(1, ParameterAttributes.In, "arg1");
            return (Func<T, object>) dynamicMethod.CreateDelegate(typeof (Func<T, object>));
        }

        internal static Action<QWriter, T> BuildParamAdder<T>()
        {
            var type = typeof(T);
            if (!IsAnonymousType(type)) throw new ApplicationException();
            
            var dynamicMethod = new DynamicMethod(name: Guid.NewGuid().ToString("N"), returnType: null,
                parameterTypes: new[] {typeof (QWriter), typeof (T)}, restrictedSkipVisibility: true);

            var ilGenerator = dynamicMethod.GetILGenerator();
            foreach (var propertyInfo in type.GetProperties())
            {
                ilGenerator.Emit(OpCodes.Ldarg_0);
                ilGenerator.Emit(OpCodes.Ldstr, "@" + propertyInfo.Name);
                ilGenerator.Emit(OpCodes.Ldarg_1);
                ilGenerator.EmitCall(OpCodes.Callvirt, propertyInfo.GetGetMethod(), null);
                ilGenerator.EmitCall(OpCodes.Callvirt,
                    QWriter.AddParamMethod.MakeGenericMethod(propertyInfo.PropertyType.GetGenericArguments()), null);
            }
            ilGenerator.Emit(OpCodes.Ret);

            dynamicMethod.DefineParameter(1, ParameterAttributes.In, "arg1");
            dynamicMethod.DefineParameter(2, ParameterAttributes.In, "arg2");
            return (Action<QWriter, T>)dynamicMethod.CreateDelegate(typeof(Action<QWriter, T>));
        }

        public static Func<T, TResult> Build<T, TResult>(Func<Type, MethodInfo> func, Func<Type, MethodInfo> singleFunc)
        {
            Type type;
            if (typeof(TResult).IsInterface)
            {
                var typeBuilder = ModuleBuilder.DefineType(
                    typeof (TResult).Name + "_" + Guid.NewGuid(), TypeAttributes.NotPublic, null,
                    new[] {typeof (TResult)});

                foreach (var property in typeof (TResult).GetProperties())
                {
                    CreateProperty(typeBuilder, property);
                }

                type = typeBuilder.CreateType();
            }
            else
            {
                type = typeof (TResult);
            }

            var dynamicMethod = new DynamicMethod(Guid.NewGuid().ToString(),
                                                  type,
                                                  new[] { typeof(T) },
                                                  true);

            var ilGenerator = dynamicMethod.GetILGenerator();
            if (IsAnonymousType(type))
            {
                var constructorInfo = type.GetConstructors()[0];
                foreach (var parameterInfo in constructorInfo.GetParameters())
                {
                    ilGenerator.Emit(OpCodes.Ldarg_0);
                    ilGenerator.Emit(OpCodes.Ldstr, parameterInfo.Name);
                    ilGenerator.EmitCall(OpCodes.Call, func(parameterInfo.ParameterType), null);                    
                }
                ilGenerator.Emit(OpCodes.Newobj, constructorInfo);
            }
            else if (type == typeof(int?) || type == typeof(Guid) || type == typeof(string)
                || type == typeof(decimal?))
            {
                ilGenerator.Emit(OpCodes.Ldarg_0);
                ilGenerator.EmitCall(OpCodes.Call, singleFunc(type), null);
            }
            else
            {
                ilGenerator.DeclareLocal(type);
                ilGenerator.Emit(OpCodes.Newobj, type.GetConstructor(new Type[] {}));
                ilGenerator.Emit(OpCodes.Stloc_0);
                var properties = type.GetProperties();
                foreach (var propertyInfo in properties)
                {
                    ilGenerator.Emit(OpCodes.Ldloc_0);
                    ilGenerator.Emit(OpCodes.Ldarg_0);
                    ilGenerator.Emit(OpCodes.Ldstr, propertyInfo.Name);
                    ilGenerator.EmitCall(OpCodes.Call, func(propertyInfo.PropertyType), null);
                    ilGenerator.EmitCall(OpCodes.Callvirt, propertyInfo.GetSetMethod(), null);
                }
                ilGenerator.Emit(OpCodes.Ldloc_0);
            }
            ilGenerator.Emit(OpCodes.Ret);

            dynamicMethod.DefineParameter(1, ParameterAttributes.In, "arg1");

            return (Func<T, TResult>)dynamicMethod.CreateDelegate(typeof(Func<T, TResult>));
        }

        private static void CreateProperty(TypeBuilder typeBuilder, PropertyInfo propertyInfo)
        {
            var fieldBuilder = typeBuilder.DefineField(
                GetPascalCase(propertyInfo.Name), propertyInfo.PropertyType, FieldAttributes.Private);

            var propertyBuilder = typeBuilder.DefineProperty(
                propertyInfo.Name, PropertyAttributes.None, propertyInfo.PropertyType, null);

            const MethodAttributes getSetMethodAttributes =
                MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig
                | MethodAttributes.SpecialName;

            var getterMethodBuilder = typeBuilder.DefineMethod("get_" + propertyInfo.Name,
                                                               getSetMethodAttributes,
                                                               propertyInfo.PropertyType,
                                                               Type.EmptyTypes);

            var getterIlGenerator = getterMethodBuilder.GetILGenerator();
            getterIlGenerator.Emit(OpCodes.Ldarg_0);
            getterIlGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
            getterIlGenerator.Emit(OpCodes.Ret);

            var setterMethodBuilder = typeBuilder.DefineMethod("set_" + propertyInfo.Name,
                                                               getSetMethodAttributes,
                                                               null,
                                                               new[] { propertyInfo.PropertyType });

            var setterIlGenerator = setterMethodBuilder.GetILGenerator();
            setterIlGenerator.Emit(OpCodes.Ldarg_0);
            setterIlGenerator.Emit(OpCodes.Ldarg_1);
            setterIlGenerator.Emit(OpCodes.Stfld, fieldBuilder);
            setterIlGenerator.Emit(OpCodes.Ret);

            propertyBuilder.SetGetMethod(getterMethodBuilder);
            propertyBuilder.SetSetMethod(setterMethodBuilder);
        }

        private static string GetPascalCase(string name)
        {
            return name.Substring(0, 1).ToLower() + name.Substring(1);
        }
    }
}